package net.barbierdereuille.android.AndroBib;

import java.io.IOException;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.util.LinkedList;
import java.util.List;


public class BibTexTokenizer {
	
	public enum Type {
		OPEN_BRACE,
		CLOSE_BRACE,
		OPEN_PAREN,
		CLOSE_PAREN,
		DQUOTE,
		AROBAS,
		EQUAL,
		TEXT,
		COMMA,
		EOF
	}
	
	private Reader mInput;
	private String mText;
	private int mNext;
	
	/**
	 * Create a new BibTeX tokenizer.
	 * @param in Buffered reader
	 * @throws IOException
	 */
	public BibTexTokenizer(Reader in) throws IOException {
		mInput = in;
		mNext = mInput.read();
	}
	
	/**
	 * Call nextToken(true)
	 * @return see nextToken(boolean)
	 * @throws IOException
	 */
	public Type nextToken() throws IOException {
		return nextToken(true);
	}
	
	/**
	 * Parse the next token
	 * @param skipSpace if true, spaces at the beginning (but not the end) are ignored
	 * @return The type of the next token
	 * @throws IOException
	 */
	public Type nextToken(boolean skipSpace) throws IOException {
		if(skipSpace) {
			while((mNext == ' ' || mNext == '\n' || mNext == '\t') && mNext != -1 ) {
				mNext = mInput.read();
			}
		}
		if(mNext == -1) return Type.EOF;
		StringBuilder builder = new StringBuilder();
		builder.append((char)mNext);
		setText(builder);
		final String tokens = "@{}()\"=,";
		char current = (char)mNext;
		mNext = mInput.read();
		switch(current)
		{
		case '@':
			return Type.AROBAS;
		case '{':
			return Type.OPEN_BRACE;
		case '}':
			return Type.CLOSE_BRACE;
		case '(':
			return Type.OPEN_PAREN;
		case ')':
			return Type.CLOSE_PAREN;
		case '"':
			return Type.DQUOTE;
		case '=':
			return Type.EQUAL;
		case ',':
			return Type.COMMA;
		default:
			builder = new StringBuilder();
			builder.append(current);
			while(mNext != -1 && tokens.indexOf(mNext) == -1) {
				builder.append((char)mNext);
				mNext = mInput.read();
			}
			setText(builder);
			return Type.TEXT;
		}
	}
	
	/**
	 * Test if a token is opening a block (i.e. is OPEN_PAREN or OPEN_BRACE)
	 * @param t Token to test
	 */
	public static boolean isOpenBlock(Type t) {
		switch(t) {
		case OPEN_PAREN:
		case OPEN_BRACE:
			return true;
		default:
			return false;	
		}
	}
	
	/**
	 * @param t Token opening the block
	 * @return the token that should close the block
	 * @throws IllegalArgumentException
	 */
	public static Type closingBlock(Type t) throws IllegalArgumentException {
		switch(t) {
		case OPEN_PAREN:
			return Type.CLOSE_PAREN;
		case OPEN_BRACE:
			return Type.CLOSE_BRACE;
		case DQUOTE:
			return Type.DQUOTE;
		default:
			throw new IllegalArgumentException("This method accept only CLOSE_PARENT, CLOSE_BRACE of DQUOTE as argument.");		
		}
	}
		
	/**
	 * @return the text of the current token
	 * @throws IOException
	 */
	public String getText() throws IOException {
		return mText;
	}
	
	/**
	 * Get the string representation of a token.
	 * @param tok Token to get the string of.
	 * @return The string associated with the token, or an empty string for EOF.
	 */
	public String getText(Type tok) {
		switch(tok) {
		case OPEN_BRACE:
			return "{";
		case CLOSE_BRACE:
			return "}";
		case OPEN_PAREN:
			return "(";
		case CLOSE_PAREN:
			return ")";
		case DQUOTE:
			return "\"";
		case AROBAS:
			return "@";
		case EQUAL:
			return "=";
		case COMMA:
			return ",";
		case TEXT:
			return "{}";
		default:
			return "";
		}
	}
	
	private void setText(StringBuilder builder) throws UnsupportedEncodingException {
		mText = new String(builder.toString());
	}

	private void setText(String txt) {
		mText = txt;
	}

	/*
	private void setText(String text) {
		mText = text;
	}
	*/
	
	/**
	 * Ignore any token until the next @ 
	 * @throws IOException
	 */
	public void skipToNextArobas() throws IOException {
		StringBuilder builder = new StringBuilder(); 
		while(mNext != -1 && mNext != '@') {
			builder.append((char)mNext);
			mNext = mInput.read();
		}
		setText(builder);
	}
	
	public void getWholeString(Type startToken) throws IOException, IllegalArgumentException {
		StringBuilder builder = new StringBuilder();
		LinkedList<Integer> blocks = new LinkedList<Integer>();
		switch(startToken) {
		case OPEN_PAREN:
			builder.append('(');
			blocks.add(Integer.valueOf(')'));
			break;
		case OPEN_BRACE:
			builder.append('{');
			blocks.add(Integer.valueOf('}'));
			break;
		case DQUOTE:
			builder.append('"');
			blocks.add(Integer.valueOf('"'));
			break;
		default:
			throw new IllegalArgumentException("Error, starting token must be OPEN_PAREN, OPEN_BRACE or DQUOTE");
		}
		do {
			builder.append((char)mNext);
			if(mNext == blocks.getLast())
				blocks.removeLast();
			else if(mNext == '{')
				blocks.add(Integer.valueOf('}'));
			mNext = mInput.read();
			if(mNext == -1) break;
		} while(!blocks.isEmpty());
		setText(builder);
	}

	public void skipBlock() throws IOException {
		StringBuilder builder = new StringBuilder();
		// First, look for start of block, i.e. {, ( or "
		do {
			mNext = mInput.read();
		} while(mNext != -1 && mNext != '{' && mNext != '(' && mNext != '"');
		if(mNext == -1) return;
		LinkedList<Integer> blocks = new LinkedList<Integer>();
		if(mNext == '{')
			blocks.add(Integer.valueOf('}'));
		else if(mNext == '(')
			blocks.add(Integer.valueOf(')'));
		else
			blocks.add(Integer.valueOf('"'));
		builder.append((char)mNext);
		do {
			mNext = mInput.read();
			if(mNext == -1) break;
			builder.append((char)mNext);
			if(mNext == blocks.getLast().intValue())
				blocks.removeLast();
			else if(mNext == '{')
				blocks.add(Integer.valueOf('}'));
			else if(mNext == '(')
				blocks.add(Integer.valueOf(')'));
			else if(mNext == '"')
				blocks.add(Integer.valueOf('"'));
		} while(!blocks.isEmpty());
		setText(builder);
	}
	
	/**
	 * Read the tokens until one in ends is found, or EOF
	 * @param ends List of token that will stop reading
	 * @return The last token read. It will be either a token in ends or EOF.
	 * @throws IOException
	 */
	public Type readUntil(List<Type> ends) throws IOException {
		String text = "";
		BibTexTokenizer.Type tok = nextToken();
		while(tok != Type.EOF && !ends.contains(tok))
		{
			if(BibTexTokenizer.isOpenBlock(tok)) {
				getWholeString(tok);
				text = text + getText();
			} else
				text = text + getText();
			tok = nextToken();
		}
		setText(text);
		return tok;
	}
}
